<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./assets/global.css">

    <style>
        #face {
            position: absolute;
            transform-origin: center;
        }

        .face-config {
            display: flex;
            flex-direction: column;
        }

        .face-config .form-item {
            display: flex;
            align-items: center;
        }

        .face-config .form-item__label {
            width: 80px;
            margin-right: 10px;
            text-align: right;
        }

        .face-config .form-item__value {
            display: flex;
            align-items: center;
        }

        .face-config .form-item__value span {
            margin-left: 10px;
        }

        .face-config .form-item__value input[type='file']+span {
            display: none;
        }

        .emo-box {
            display: flex;
        }
    </style>
</head>

<body>
    <div class="emo-box">
        <canvas id="emo"></canvas>
        <canvas id="face"></canvas>
        <div class="face-config">
            <div class="form-item">
                <h3>表情包生成器</h3>
            </div>

        </div>
    </div>



    <script src="./assets/face-api/face-api.min.js"></script>
    <script type="module">
        import "https://gcore.jsdelivr.net/npm/@3r/canvas-plus@1.0.8/index.js";


        /**
         * 创建
         * @author 	 linyisonger
         * @date 	 2025-04-02
         */
        function createFormItemDOM({ label, max, min, step, change, type, value }) {
            let formItemDOM = document.createElement('div')
            let formItemLabelDOM = document.createElement('div')
            let formItemValueDOM = document.createElement('div')
            let formItemValueInputDOM = document.createElement('input')
            let formItemValueTxtDOM = document.createElement('span')
            formItemDOM.className = 'form-item'
            formItemLabelDOM.className = 'form-item__label'
            formItemValueDOM.className = 'form-item__value'
            formItemValueTxtDOM.className = 'form-item__value-txt'
            formItemLabelDOM.textContent = label
            formItemValueTxtDOM.textContent = value
            formItemValueInputDOM.type = type
            formItemValueInputDOM.min = min || 0;
            formItemValueInputDOM.max = max || 1;
            formItemValueInputDOM.step = step || 0.01;
            formItemValueInputDOM.setAttribute('value', value)

            formItemValueDOM.appendChild(formItemValueInputDOM)
            formItemValueDOM.appendChild(formItemValueTxtDOM)
            formItemDOM.appendChild(formItemLabelDOM)
            formItemDOM.appendChild(formItemValueDOM)

            formItemValueInputDOM.addEventListener('change', (e) => {
                formItemValueTxtDOM.textContent = e.target.value
                let v = type === 'range' ? +e.target.value :
                    type === 'file' ? e.target.files : e.target.value
                change?.(v)
            })
            return formItemDOM
        }

        function createButtonDOM({ label, click }) {
            let buttonDOM = document.createElement('button')
            buttonDOM.textContent = label
            buttonDOM.addEventListener('click', click)
            return buttonDOM;
        }

        /**
         * 靠近一点点
         * @author 	 linyisonger
         * @date 	 2025-03-24
         * @param {{x:number,y:number}} p
         * @param {{x:number,y:number}} t
         * @param {number} v
         */
        function zoom(p, t, v) {
            let tmpX = p.x - t.x;
            let tmpY = p.y - t.y;

            tmpX *= v;
            tmpY *= v;

            return {
                x: t.x + tmpX,
                y: t.y + tmpY,
            }
        }


        /**
         * 加载图
         * @param {string} src
         * @returns {Promise<HTMLImageElement>}
         */
        function loadImage(src) {
            return new Promise((resolve) => {
                let image = new Image()
                image.src = src;
                image.onload = (ev) => {
                    resolve(image)
                }
            })
        }

        function saveImage(blob) {
            let a = document.createElement('a');
            a.href = window.URL.createObjectURL(blob);
            a.setAttribute('download', '表情包.jpg');
            a.click();
        }
        /**
         * 绘制表情包的背景
         * @author 	 linyisonger
         * @date 	 2025-03-24
         */
        async function drawEmoBackground() {
            /** @type {HTMLCanvasElement} */
            let canvas = document.querySelector("#emo")
            let ctx = canvas.getContext('2d')
            let pandaHead = await loadImage('./assets/pandaHead.png')
            let width = pandaHead.width;
            let height = pandaHead.height;
            canvas.width = width;
            canvas.height = height;
            ctx.clearRect(0, 0, width, height)
            // ctx.fillStyle = '#999'
            // ctx.fillRect(0, 0, width, height)
            ctx.drawImage(pandaHead, 0, 0, width, height);
        }

        /**
         * 获取面部信息
         * @author 	 linyisonger
         * @date 	 2025-03-24
         */
        async function drawFaceImage(url) {
            const originImg = await faceapi.fetchImage(url) // 获取原图
            const detections = await faceapi.detectSingleFace(originImg).withFaceLandmarks() // 获取特征
            const positions = detections.landmarks.positions; // 面部坐标点位
            const linkPositionIndexList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17] // 外轮廓
            const zoomPositionIndexList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] // 脸颊
            /** @type {HTMLCanvasElement} */
            let canvas = document.querySelector("#face")
            let ctx = canvas.getContext('2d')
            let width = originImg.width;
            let height = originImg.height;
            canvas.width = width;
            canvas.height = height;

            for (let i = 0; i < zoomPositionIndexList.length; i++) {
                const zoomPosition = positions[zoomPositionIndexList[i]];
                positions[zoomPositionIndexList[i]] = zoom(zoomPosition, positions[29], .9) // 缩放脸颊 清除黑边
            }

            // 裁切操作
            ctx.beginPath();
            ctx.strokeStyle = '#000'
            ctx.lineWidth = 2;
            for (let i = 0; i < linkPositionIndexList.length; i++) {
                const linkPosition = positions[linkPositionIndexList[i]];
                i == 0 ? ctx.moveTo(linkPosition.x, linkPosition.y) : ctx.lineTo(linkPosition.x, linkPosition.y)
            }
            ctx.closePath();
            ctx.clip();
            ctx.drawImage(originImg, 0, 0, width, height); // 渲染原图 
            ctx.grayProcessing({}) // 灰度处理
            let layerSizeRes = ctx.layerSize({}) // 获取尺寸
            faceImageData = ctx.getImageData(layerSizeRes.x, layerSizeRes.y, layerSizeRes.w, layerSizeRes.h)
            ctx.clearRect(0, 0, width, height)
            width = layerSizeRes.w;
            height = layerSizeRes.h;
            canvas.width = width
            canvas.height = height
            ctx.putImageData(faceImageData, 0, 0)
            // ctx.fillStyle = '#333'
            // ctx.fillRect(0, 0, width, height)
        }

        async function drawFaceEffect() {
            /** @type {HTMLCanvasElement} */
            let canvas = document.querySelector("#face")
            let ctx = canvas.getContext('2d')
            ctx.clearRect(0, 0, canvas.width, canvas.height)
            ctx.putImageData(faceImageData, 0, 0)
            ctx.luminance({ v: faceConfig.luminance })
            ctx.exposure({ v: faceConfig.exposure })
            ctx.contrastRatio({ v: faceConfig.contrastRatio })
        }

        function updateFaceTranform() {
            console.log('updateFaceTranform');
            let emoDOM = document.querySelector('#emo')
            let faceDOM = document.querySelector('#face')
            let width = emoDOM.width;
            let height = emoDOM.height;
            faceDOM.setAttribute('style', `left:${faceConfig.x * width}px;top:${faceConfig.y * height}px;width:${width * faceConfig.w}px;height:${height * faceConfig.h}px;transform: rotate(${faceConfig.a}deg)`)
        }


        Promise.all([
            faceapi.nets.faceRecognitionNet.loadFromUri('./assets/face-api/models'),
            faceapi.nets.faceLandmark68Net.loadFromUri('./assets/face-api/models'),
            faceapi.nets.ssdMobilenetv1.loadFromUri('./assets/face-api/models')
        ]).then(start)


        // 面部位置配置
        // 比例
        const faceConfig = {
            x: 0.29,
            y: 0.26,
            w: 0.45,
            h: 0.51,
            a: 0, // 旋转角度
            url: './assets/stars/jiangshuying.jpg', // 默认照片
            luminance: -0.5, // 亮度
            exposure: 4, // 曝光
            contrastRatio: 2, // 对比度
        }
        let faceImageData = null
        let faceConfigDOM = document.querySelector('.face-config')

        async function start() {
            faceConfigDOM.appendChild(createFormItemDOM({ label: 'x:', type: 'range', value: faceConfig.x, change: (v) => (faceConfig.x = v) && updateFaceTranform() }))
            faceConfigDOM.appendChild(createFormItemDOM({ label: 'y:', type: 'range', value: faceConfig.y, change: (v) => (faceConfig.y = v) && updateFaceTranform() }))
            faceConfigDOM.appendChild(createFormItemDOM({ label: 'w:', type: 'range', value: faceConfig.w, change: (v) => (faceConfig.w = v) && updateFaceTranform() }))
            faceConfigDOM.appendChild(createFormItemDOM({ label: 'h:', type: 'range', value: faceConfig.h, change: (v) => (faceConfig.h = v) && updateFaceTranform() }))
            faceConfigDOM.appendChild(createFormItemDOM({ label: 'a:', type: 'range', value: faceConfig.a, change: (v) => (faceConfig.a = v) && updateFaceTranform(), min: 0, max: 360, step: 1, }))
            faceConfigDOM.appendChild(createFormItemDOM({
                label: '照片:', type: 'file', value: '', change: async (v) => {
                    const filePath = URL.createObjectURL(v[0])
                    await drawFaceImage(filePath)
                    drawFaceEffect()
                }
            }))
            faceConfigDOM.appendChild(createFormItemDOM({ label: '亮度:', type: 'range', value: faceConfig.luminance, change: (v) => (faceConfig.luminance = v) && drawFaceEffect(), min: -10, max: 10, step: .1, }))
            faceConfigDOM.appendChild(createFormItemDOM({ label: '曝光:', type: 'range', value: faceConfig.exposure, change: (v) => (faceConfig.exposure = v) && drawFaceEffect(), min: -10, max: 10, step: .1, }))
            faceConfigDOM.appendChild(createFormItemDOM({ label: '对比度:', type: 'range', value: faceConfig.contrastRatio, change: (v) => (faceConfig.contrastRatio = v) && drawFaceEffect(), min: -10, max: 10, step: .1, }))
            faceConfigDOM.appendChild(createButtonDOM({
                label: '保存', click: async () => {
                    console.log('保存图片');
                    /** @type {HTMLCanvasElement} */
                    let faceDOM = document.querySelector('#face')
                    /** @type {HTMLCanvasElement} */
                    let canvas = document.querySelector("#emo")
                    let ctx = canvas.getContext('2d')

                    let x = faceConfig.x * canvas.width;
                    let y = faceConfig.y * canvas.height;
                    let w = faceConfig.w * canvas.width;
                    let h = faceConfig.h * canvas.height;
                    let rad = faceConfig.a / 180 * Math.PI;

                    let cx = x + w / 2
                    let cy = y + h / 2

                    let faceImage = await loadImage(faceDOM.toDataURL('image/png', .8))

                    console.log(cx, cy);

                    // ctx.drawImage(faceImage, x, y, w, h)

                    ctx.translate(cx, cy)
                    ctx.rotate(rad)
                    ctx.translate(-cx, -cy)
                    ctx.drawImage(faceImage, cx - w / 2, cy - h / 2, w, h)
                    ctx.translate(cx, cy)
                    ctx.rotate(-rad)
                    ctx.translate(-cx, -cy)

                    canvas.toBlob((b) => saveImage(b), 'image/jpeg', .8)
                    drawEmoBackground()
                }
            }))

            drawEmoBackground()
            await drawFaceImage(faceConfig.url)
            drawFaceEffect()
            updateFaceTranform();
        }

    </script>




</body>

</html>